<html>

<head>
	<title>PICO-8 Education Edition</title>
	<meta charset="utf-8" />
	<meta property="og:title" content="PICO-8 Education Edition" />
	<meta name="viewport" content="width=device-width">

<!--	<script type="text/javascript">-->
<!--		function microAjax(B, A) { this.bindFunction = function (E, D) { return function () { return E.apply(D, [D]) } }; this.stateChange = function (D) { if (this.request.readyState == 4) { this.callbackFunction(this.request.responseText) } }; this.getRequest = function () { if (window.ActiveXObject) { return new ActiveXObject("Microsoft.XMLHTTP") } else { if (window.XMLHttpRequest) { return new XMLHttpRequest() } } return false }; this.postBody = (arguments[2] || ""); this.callbackFunction = A; this.url = B; this.request = this.getRequest(); if (this.request) { var C = this.request; C.onreadystatechange = this.bindFunction(this.stateChange, this); if (this.postBody !== "") { C.open("POST", B, true); C.setRequestHeader("X-Requested-With", "XMLHttpRequest"); C.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); C.setRequestHeader("Connection", "close") } else { C.open("GET", B, true) } C.send(this.postBody) } };-->
<!--	</script>-->


	<STYLE TYPE="text/css">
		<!--
		@font-face {
			font-family: 'proggyvec';
			src: url('https://www.pico-8-edu.com/proggyvec.woff') format('woff');

		}

		.banner_div {
			display: block
		}

		.topmenu_div {
			display: table
		}

		.lilmenu_div {
			display: none
		}

		.desktop_div {
			display: inline
		}

		.mobile_div {
			display: none
		}

		.avatar {
			border-radius: 10%;
			box-shadow: 4px 4px 6px rgba(0.2, 0.0, 0.4, 0.1);
		}

		.avatar_tiny {
			border-radius: 20%;
			box-shadow: 4px 4px 6px rgba(0.2, 0.0, 0.4, 0.1);
		}

		.scrollable_with_touch {
			max-width: 95vw;
			overflow-x: auto;
			overflow-y: hidden;
			-webkit-overflow-scrolling: touch;
		}

		.info_box {
			display: table;
			float: left;
			margin-left: 8px;
			margin-right: 16px;
			margin-top: 0px;
			margin-bottom: 16px;
			text-align: left;
		}

		.info_box_right_old {
			display: table;
			margin-left: 16px;
			margin-right: 8px;
			margin-top: -10px;
			margin-bottom: 16px;
			float: right;
			text-align: left;
		}

		.info_box_right {
			margin-top: -10px;
		}


		.info_box h2 {
			margin-top: -16px;
		}

		h2 {
			margin-top: 0px;
		}

		.info_group {
			display: table;
			max-width: 800px;
			margin-bottom: 20px;
			text-align: left
		}

		.info_group img {
			min-width: 192px;
		}

		.info_group h2 {
			text-decoration: none;
			color: #fab;
			font-weight: normal;
		}


		.lexchex {
			padding: 8px;
			color: #444;
			font-size: 12pt;
		}

		.lexchex input {
			margin-right: 6px;
			height: 24px;
			width: 24px;
			background-color: #eee;
			position: relative;
			top: 4px;
		}

		.pip_form_input {
			background: #222;
			color: #ccc;
		}


		.form_button {
			padding: 6px;
			padding-left: 10px;
			padding-right: 10px;
			display: table;
			margin-top: 12px;
			border: 2px solid;
			border-color: #999;
			border-radius: 6px;
			background: #222;
			color: #ccc;
		}

		.form_button:hover {
			background-color: #fab;
			color: #222;
			cursor: pointer;
		}

		.form_button_span {
			padding: 2px;
			padding-left: 4px;
			padding-right: 4px;
			display: inline;
			border: 1px solid;
			border-color: #999;
		}

		.form_button_span:hover {
			background-color: #fab;
			color: #222;
			cursor: pointer;
		}

		.edit_tool_button {
			padding: 4px;
			display: table;
			padding-left: 8px;
			padding-right: 8px;
			border-radius: 1;
			margin-right: 4px;
			margin-bottom: 2px;
			border: 1px solid;
			border-color: #999;
			margin-bottom: 8px;
		}

		.edit_tool_button:hover {
			background-color: #fab;
			color: #222;
			cursor: pointer;
		}


		.form_tab {
			padding: 12px;
			padding-right: 20px;
			padding-top: 8px;
			padding-bottom: 8px;
			display: table;
			float: left;
			font-size: 14pt;
			border-radius: 0px;
			margin-right: 2px;
			color: #ccc;
			background-color: #444;
		}

		.form_tab:hover {
			background-color: #f8a;
			color: #fff;
			cursor: pointer;
		}


		zzz_canvas {
			image-rendering: optimizeSpeed;
			image-rendering: -moz-crisp-edges;
			image-rendering: -webkit-optimize-contrast;
			image-rendering: optimize-contrast;
			image-rendering: pixelated;
			-ms-interpolation-mode: nearest-neighbor;
			border: 0px;
			width: 512px;
			height: 512px;
		}

		.favlike {
			min-width: 160px;
		}

		.pixel_perfect {
			image-rendering: optimizeSpeed;
			image-rendering: -moz-crisp-edges;
			image-rendering: -webkit-optimize-contrast;
			image-rendering: optimize-contrast;
			image-rendering: pixelated;
			-ms-interpolation-mode: nearest-neighbor;
			border: 0px;
		}

		.subnav {
			float: right;
			max-width: 400px;
			margin-top: 10px
		}

		.snb {
			padding: 6px;
			margin: 2px;
			display: flex;
		}

		@media screen and (max-width: 800px) {
			.subnav {
				float: left;
				width: 100%;
			}

			.snb {
				padding: 5px;
			}
		}

		@media screen and (max-width: 920px) {
			.banner_div {
				display: none;
			}

			.topmenu_div {
				display: none;
			}

			.lilmenu_div {
				display: table;
			}

			.desktop_div {
				display: none;
			}

			.mobile_div {
				display: inline;
			}

			.info_box {
				float: left;
				width: 100%;
				margin: 0px;
				margin-bottom: 10px
			}

			.info_box_right {
				float: left;
				width: 100%;
				margin: 0px;
				margin-bottom: 10px
			}

			.favlike {
				min-width: 10px;
			}
		}

		.block {
			text-align: center;
			margin: 20px;
		}

		.centered {
			display: inline-block;
			vertical-align: middle;
			width: 300px;
		}



		img {
			max-width: 100%;
			height: auto;
		}

		body {
			font-size: 11pt;
			font-family: proggyvec;
			line-height: 1.4;

			color: #ccc;
			background-color: #303030;
			margin: 0px;
			padding: 0px;
		}

		code {
			padding: 2px;
			margin: 2px;
			padding-left: 4px;
			padding-right: 4px;
			background-color: #ddd;
			color: #222;
		}


		TD {
			font-size: 11pt;
		}

		p {
			font-size: 11pt;
		}

		pre {
			font-size: 11pt;
		}

		code {
			font-size: 11pt;
		}

		a:hover {
			cursor: pointer;
		}

		b {
			color: #ffffff
		}

		.tag {
			float: left;
			display: table;
			text-align: left;
			font-size: 8pt;
			margin-right: 4px;
			margin-bottom: 4px;
			padding: 3px;
			padding-left: 6px;
			padding-right: 6px;
			background-color: #aaa;
			color: #444;

			border-radius: 2px;
		}

		.tag:hover {
			cursor: pointer;
			color: #fff;
			background-color: #f8a;
		}


		.tagblue {
			font-size: 8pt;
			padding: 4px;
			padding-left: 6px;
			padding-right: 6px;
			background-color: #8ad;
			color: #222;
			margin-top: 6px;
			display: inline-block;
			border-radius: 2px;

		}

		.tagblue:hover {
			cursor: pointer;
			color: #fff;
			background-color: #f8a;
		}

		.bright_hover {
			cursor: pointer;
			opacity: 0.3;
		}

		.bright_hover:hover {
			opacity: 1.0;
		}

		.menu_icon_lil {
			display: inline-block;
			padding-top: 4px;
			padding-left: 2px;
			padding-right: 2px;
		}

		.menu_icon {
			display: inline-block;
			padding-top: 4px;
			padding-left: 2px;
			padding-right: 2px;
		}

		.menu_icon a {
			display: inline-block;
			opacity: 0.5;
		}

		.menu_icon a:hover {
			opacity: 1.0;
			background: #ff80a0;
		}

		.menu_icon hover {
			opacity: 1.0;
			background: #ff80a0;
		}

		.thread_preview {
			width: 100%;
			min-height: 64px;
			margin-top: 2px;
			background-color: #444;
		}

		.stitched {
			padding: 10px;
			padding-left: 20px;
			padding-right: 20px;
			margin: 10px;
			margin-right: 50px;
			background: #f8f8f8;
			color: #404040;
			font-size: 21px;
			font-weight: bold;
			line-height: 0.5em;
			border: 2px dashed #d0e0ff;
			border-radius: 5px;
			box-shadow: 0 0 0 4px #f8f8f8, 2px 1px 6px 4px rgba(5, 5, 0, 0.2);
			font-weight: normal;
		}

		.social_button {
			opacity: 0.5;
		}

		.social_button:hover {
			opacity: 1.0;
		}

		.support_button {
			padding: 8px;
			background-color: #fab;
			color: #333;
			font-size: 10pt;
			display: table;
			opacity: 1.0;
		}

		.support_button:hover {
			background-color: #fab;
			opacity: 1.0;
		}




		.pinkhover {
			cursor: pointer;
		}

		.pinkhover:hover {
			background: #fab;
			color: #222;
		}

		.pink-hover_t a {
			opacity: 0.5;
		}

		.pink-hover_t2 a {}

		.pink-hover a:hover {
			display: block;
			background: #ff80a0;
			opacity: 1.0;
		}

		.pink-hover_t a:hover {
			display: block;
			background: #ff80a0;
			opacity: 1.0;
		}



		.post_title {
			font-size: 9pt;
		}

		.post_title2 {
			font-size: 20pt;
			color: #ffffff;
			margin: 0px;
			margin-bottom: 8px;
			margin-top: 0px;
		}

		@media screen and (max-width: 800px) {
			.post_title2 {
				font-size: 16pt
			}

			;
		}

		.breadcrumbs {
			margin-left: 8px;
			font-size: 12px;
			color: #888;
			border-radius: 0px;
		}

		.breadcrumbs a:link {
			color: #ccc;

			padding: 4px;

			display: inline-block;
		}

		.breadcrumbs a:visited {
			color: #ccc;
			background-color: #666;
		}

		.breadcrumbs a:hover {
			color: #fff;
			background-color: #888;
		}



		.breadcrumbs2 {
			margin-left: 8px;
			font-size: 12px;
			color: #888;
			border-radius: 0px;
		}

		.breadcrumbs2 a:link {
			color: #999;
			padding: 4px;
			padding-left: 8px;
			padding-right: 8px;
			display: inline-block;
		}

		.breadcrumbs2 a:visited {
			color: #999;
		}

		.breadcrumbs2 a:hover {
			color: #fab;
		}



		.post_title3 {
			font-size: 12pt;
			font-weight: bold;
			color: #ff3070;
		}

		.blog_title2 {
			font-size: 20pt;
			color: #ff3070;
		}

		.blog_title {
			font-size: 16pt;
			color: #202020;

		}

		.blog_num_comments {
			font-size: 14pt;
			color: #303030;
			font-weight: bold;
		}

		.user_title {
			font-size: 12pt;
			color: #ffe0d0;
		}

		h1 {
			color: #ffffff;
			font-size: 22pt;
			padding-top: 8px
		}

		h2 {
			color: #ffffff;
			font-size: 18pt;
			padding-top: 16px
		}

		h3 {
			color: #f7a;
			font-size: 14pt;
			padding-top: 8px
		}


		A:link {
			text-decoration: none;
			color: #fff;
		}

		A:visited {
			text-decoration: none;
			color: #fff;
		}

		p A:link {
			zzz_text-decoration: underline;
			color: #fbc;
		}

		p A:hover {
			color: #f0f;
		}

		p A:visited {
			zzz_text-decoration: underline;
			color: #fbc;
		}

		p A:visited:hover {
			color: #fff;
		}

		.link_with_image_unused {
			background-image: url("/gfx/ext_link.png");
			background-repeat: no-repeat;
			background-position: right;
			padding-right: 20px;
			color: #fff;
		}



		A:hover {
			text-decoration: none;
			color: #fff;
		}

		A:active {
			text-decoration: none;
			color: #ffffff
		}

		.navstring a {
			color: #808080;
			font-weight: bold;
		}

		.navstring a:hover {
			color: #faa;
			font-weight: bold;
		}

		.navstring {
			color: #c0c0c0;
			font-weight: bold;
		}



		.linkydiv {
			display: block;
			text-align: center;
			vertical-align: middle;
			margin: 0px;
			background-color: #ff80a0;
		}

		.linkydiv:hover {
			background-color: #ff80a0;
			color: #ffffff;
		}

		.cartfile {
			float: left;
			width: 160px;
			height: 200px;
			margin: 4px;
			padding: 4px;
			text-align: center;
		}

		.cartlabel {
			background: #333;
			width: 124;

			padding: 2px;
			margin-top: 8px;
			padding-top: 4px;
			padding-bottom: 4px;
			font-size: 10px;
		}

		.cartlabelbig {
			background: #333;
			width: 252;

			padding-top: 14px;
			padding-bottom: 14px;
			margin-top: 16px;
			font-size: 16px;
		}



		.linkybutton {
			float: left;
			font-size: 14px;
			color: #808080;
			display: table-cell;
			text-align: center;
			vertical-align: middle;
			padding-top: 10px;
			padding-bottom: 10px;
			padding-left: 12px;
			padding-right: 12px;
			margin: 0px;
			height: 16px;
		}


		@media screen and (max-width: 800px) {
			.linkybutton {
				float: none;
				display: flex;
				padding: 12px;

				align-text: left;
				margin-left: 50px;
				font-size: 11pt;
			}
		}

		.linkbutton.img img {
			display: inline-block;
			vertical-align: middle;

		}

		.linkybutton:hover {
			background-color: #ff80a0;
			color: #ffffff;
		}

		.linkybutton_lil0 {
			float: left;
			background-color: #444;
			padding: 6px;
			color: #444;
			font-size: 14px;
			height: 24px;
			vertical-align: center;
			display: inline-table;
			margin: 2px;
		}

		.linkybutton_lil {
			float: left;
			background-color: #555;
			padding: 8px;
			color: #aaa;
			font-size: 14px;
			height: 20px;
			vertical-align: center;
			display: inline-table;
			margin: 2px;
		}

		.linkybutton_lil:hover {
			background-color: #ff80a0;
			color: #ffffff;
		}

		.pinkbutton {
			background: #505050;
			padding: 6px;
		}

		.pinkbutton:hover {
			background-color: #808080;
		}


		.pbutton {
			padding: 0px;
			margin: 0px;
			background-color: #505050;
			color: #a0a0a0;
		}

		.pbutton:hover {
			background-color: #707070;
		}


		.logininput {
			font-size: 12px;
			color: #000000;
			line-height: 14px;
			border: 0;
			margin: 2px;
			padding: 2px;
			background-color: #ccc;
			height: 30px;
		}

		.logininput:hover {
			background-color: #ff80a0;
		}

		.logininput2 {
			font-size: 12px;
			color: #fff;
			border-width: 1px;
			border-style: solid;
			border-color: #fff;
			border-radius: 3px;
			margin: 4px;
			padding: 8px;
			padding-left: 16px;
			padding-right: 16px;
			background-color: #333;
			height: 32px;
			cursor: pointer;
		}

		.logininput2:hover {
			background-color: #ff80a0;
		}

		input {
			background-color: #ffffe0;
			color: #202020;
			border-width: 1px;
			border-style: solid;
			border-color: #999999;
			font-size: 12pt;
			color: #404040;
		}


		.lexinput_search {
			background-color: #999;
			color: #202020;
			font-size: 10pt;
			color: #111;
			height: 20px;
			margin-right: 8px;
		}

		.lexinput {
			background-color: #ffffe0;
			color: #202020;
			border-width: 1px;
			border-style: solid;
			border-color: #999999;
			font-size: 12pt;
			color: #404040;
			height: 32px;
			margin-left: 8px;
		}

		.result_msg {
			color: #888;
		}


		#login_button {
			width: 48px;
			height: 16px;
			margin: 0;
			padding: 0;
			border: 0;
			background: transparent url(img/login_button.png) no-repeat center top;
			text-indent: -1000em;
		}

		#signup_button {
			width: 48px;
			height: 16px;
			margin: 0;
			padding: 0;
			border: 0;
			background: transparent url(img/signup_button.png) no-repeat center top;
			text-indent: -1000em;
		}
		-->
	</STYLE>

	<script>
		var current_time = "2025-06-28 06:22:32";
		var s_uid = 0;

		function $(name) {
			return document.getElementById(name);
		}

		// globals

		var p_page = 1;
		var p_sub = 0;
		var p_cat = 0;
		var p_pid = 0;
		var p_mode = 0;
		var search_url_base = "";
		var tag_url_base = "";
		var pip_form_data = {};
		var touch_detected = false;
		var undefined = "undefined"; // :/
		var current_playing_cart_id = -1;


		function microAjax(B, A) { this.bindFunction = function (E, D) { return function () { return E.apply(D, [D]) } }; this.stateChange = function (D) { if (this.request.readyState == 4) { this.callbackFunction(this.request.responseText) } }; this.getRequest = function () { if (window.ActiveXObject) { return new ActiveXObject("Microsoft.XMLHTTP") } else { if (window.XMLHttpRequest) { return new XMLHttpRequest() } } return false }; this.postBody = (arguments[2] || ""); this.callbackFunction = A; this.url = B; this.request = this.getRequest(); if (this.request) { var C = this.request; C.onreadystatechange = this.bindFunction(this.stateChange, this); if (this.postBody !== "") { C.open("POST", B, true); C.setRequestHeader("X-Requested-With", "XMLHttpRequest"); C.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); C.setRequestHeader("Connection", "close") } else { C.open("GET", B, true) } C.send(this.postBody) } };

		// from: https://plainjs.com/javascript/ajax/send-ajax-get-and-post-requests-47/
		function post_ajax(url, data, success) {
			var params = typeof data == 'string' ? data : Object.keys(data).map(
				function (k) { return encodeURIComponent(k) + '=' + encodeURIComponent(data[k]) }
			).join('&');

			var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP");
			xhr.open('POST', url);
			xhr.onreadystatechange = function () {
				if (xhr.readyState > 3 && xhr.status == 200) { success(xhr.responseText); }
			};
			xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
			//xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
			xhr.setRequestHeader('Content-Type', 'text/plain; charset=UTF-8');

			xhr.send(params);
			return xhr;
		}

		// in ms.  1000, 20 means 50fps for 1 second.
		function poll_function(duration, interval, func) {
			var start_t = (new Date()).getTime();
			(function p() {
				tt = (new Date).getTime() - start_t;
				if (tt <= duration) {
					func(tt / duration);
					setTimeout(p, interval);
				}
			})();
		}


		function insertTextAtCursor(el, text, overwrite) {
			if (typeof el == "string") {
				el_str = el;
				el = document.getElementById(el);
				if (!el) {
					console.log("@ could not find element to insert text: " + el_str);
				}
			}

			var val = el.value, endIndex, range, doc = el.ownerDocument;

			if (overwrite) {
				el.focus();
				el.value = text;
				return;
			}

			if (typeof el.selectionStart == "number"
				&& typeof el.selectionEnd == "number") {
				endIndex = el.selectionEnd;
				el.value = val.slice(0, endIndex) + text + val.slice(endIndex);
				el.selectionStart = el.selectionEnd = endIndex + text.length;
			}
			else if (doc.selection != "undefined" && doc.selection.createRange) {
				el.focus();
				range = doc.selection.createRange();
				range.collapse(false);
				range.text = text;
				range.select();
			}


		}



		function set_enter_to_save(el, save_el) {
			if (!el) return;

			// experimental: only auto-save when value has changed (can tab through unchanged fields)
			var start_val = el.value;

			if (!el.installed_enter_to_save) // only install once
			{
				console.log("@installing set_enter_to_save: " + el + ", " + save_el);

				el.installed_enter_to_save = true;
				el.addEventListener("keyup",
					function (event) {
						//console.log(event.keyCode);

						if (event.keyCode == 13)
							//if (el.value != start_val) // commented --  don't need / confusing.
							save_el.click();


						//if (event.keyCode == 9) // tab  // commented -- use blur if want to do this
						//	save_el.click();
					}
				);

				// also auto-save on tab / click off

				// careful not to introduce double sends (hard to get right -- maybe not worth it)
				el.addEventListener("blur",
					function (event) {
						var x = event.clientX, y = event.clientY;
						if (typeof x != 'number') return;
						if (typeof y != 'number') return;

						var el0 = document.elementFromPoint(x, y);
						if (!el0 || el0.name != "cancel_button")

							//if (el.value != start_val) save_el.click();

							save_el.click();
					}
				);
			}

		}


		function toggle_set_button(el_name, post_id, which, sid) {
			var el = document.getElementsByClassName(el_name);
			var eli = document.getElementsByClassName('i_' + el_name);

			if (s_uid == 0) {
				window.alert("Please log in first.");
				return;
			}

			// window.alert(el_name);

			for (var i = 0; i < eli.length; i++) {
				eli[i].innerHTML = '<center><img src=/gfx/load16.gif width=20 height=20></center>';
			}

			microAjax("/bbs/set.php?pid=" + post_id + "&sid=" + sid + "&which=" + which,

				function (retdata) {
					var el = document.getElementsByClassName(el_name);
					for (var i = 0; i < el.length; i++) {
						el[i].innerHTML = json_parse(retdata)['printable'];
					}

					if (which == "tick") {
						// location.reload(true); // bleh
					}
				}
			);
		}

		function hide_save_draft_button() {
			var el = document.getElementById("save_draft_button");
			if (el) el.style.display = "none";
			var el = document.getElementById("preview_button");
			if (el) el.style.display = "";
			var el = document.getElementById("publish_post_button");
			if (el) el.style.innerHTML = "Publish Changes";
		}

		function save_attr(val_id, div_id, url, post_save, extract_snippets) {
			if (extract_snippets)
				url = url + '&extract_snippets=1';

			//console.log("@ save_attr("+val_id+", "+div_id+", "+url+")");
			//console.log("  @  extract_snippets: "+extract_snippets);

			var el1 = document.getElementById(div_id);
			var el = document.getElementById(val_id);

			if (el) {
				//console.log(" @ sending:" + el.value);

				value = el.value;

				post_ajax(url, el.value,
					function (retdata) {
						dat = json_parse(retdata);
						el1.innerHTML = dat['printable'];

						// set value field
						if (dat['modified'] && dat['status'] == "ok")
							el.value = dat['modified'];
						//else
						//	console.log("  @ no modified text");

						if (post_save)
							post_save();
					}
				);
			}
			else {
				// grab value from url (val=) instead of body. e.g. catsub buttons
				microAjax(url,
					function (retdata) {
						el1.innerHTML = json_parse(retdata)['printable'];
					}
				)
			}

			//el1.innerHTML = '<img src=/gfx/load16.gif>';
			el1.innerHTML = '<div style="padding:8px"><img src=/gfx/load16.gif></div>';

		}


		function cancel_attr(val_id, div_id, url) {
			console.log("@ cancel_attr(" + val_id + ", " + div_id + ", " + url);

			var el = document.getElementById(val_id);
			if (el) {
				var el1 = document.getElementById(div_id);
				el1.innerHTML = '<div style="padding:8px"><img src=/gfx/load16.gif></div>';

				microAjax(url,
					function (retdata) {
						el1.innerHTML = json_parse(retdata)['printable'];;
					}
				);
			}
		}


		function edit_attr(val_id, div_id, save_id, url, has_autosave) {
			//console.log("@ edit_attr("+val_id+", "+div_id+", "+url+", "+has_autosave);

			var el = document.getElementById(div_id);

			if (el) {
				//el.innerHTML='<img src=/gfx/load16.gif>';//+el.innerHTML;
				el.innerHTML = '<div style="padding:8px"><img src=/gfx/load16.gif></div>';

				microAjax(url,
					function (retdata) {
						el.innerHTML = json_parse(retdata)['printable'];

						var el2 = document.getElementById(val_id);

						if (el2)
							if (has_autosave) {
								el2.focus();
								//el2.select();
								var el3 = document.getElementById(save_id);
								set_enter_to_save(el2, el3);
							}
					}
				);
			}
		}



		// enter data
		function submit_pip_form_data(form) {
			// for each class=pip_form_input element, add to global pip_form_data

			var els = document.getElementsByClassName('pip_form_input');

			console.log("@ submit form: adding " + els.length + " values");

			for (var i = 0; i < els.length; i++) {
				el = els[i];

				if (el.checked) {
					pip_form_data[el.id] = "checked";
				}
				else if (el.value) {
					pip_form_data[el.id] = el.value;
				}

				console.log("@ set " + el.id + " to " + pip_form_data[el.id]);

				if (el.files && el.files[0]) {
					console.log("@ file: " + el.files[0].name);

					pip_form_data["uploaded_file"] = JSON.stringify(el.files[0]);
					pip_form_data["uploaded_file_size"] = el.files[0].size;
					pip_form_data["uploaded_name"] = el.files[0].name;
					pip_form_data["uploaded_type"] = el.files[0].type;
				}
			}
		}

		function reload_pip_form(form, step) {
			var el = document.getElementById(form);

			console.log("reloading form: " + form + " // step " + step);

			if (el) {

				var file_size = 0;
				if (pip_form_data.file_contents)
					file_size = pip_form_data.file_contents.length * 6 / 8;

				if (form == "cart_submit" && file_size > 1024 * 1024) // 1MB for pico-8 / voxatron carts
				{
					window.alert('Sorry, that file is too large. (' + Math.floor(file_size / 1024) + 'k / 1024k)');
					return;
				}

				if (file_size > 1024 * 1024 * 8) {
					window.alert('Sorry, that file is too large. (' + Math.floor(file_size / (1024 * 1024)) + 'MB / 8MB)');
					return;
				}

				// upload

				stringified_data = JSON.stringify(pip_form_data);

				var el1 = document.getElementById("form_loading_icon");
				if (el1)
					el1.innerHTML = '<img src=/gfx/load16.gif>';

				//window.alert('pip_form_data.target_field:'+pip_form_data["target_field"]);

				// pass along some context
				post_ajax("/form.php?form=" + form + "&step=" + step + "&cat=" + p_cat + "&sub=" + p_sub + "&pid=" +
					p_pid + "&page=" + p_page + "&mode=" + p_mode,
					stringified_data,
					function (retdata) {
						el.innerHTML = retdata; // not JSON!
					}
				);
			}
			else {
				console.log("@@ could not find form element");
			}
		}

		function save_open_pip_form_fields() {
			// click all the save buttons
			var els = document.getElementsByClassName('form_button');
			//for(var i = 0; i < els.length; i++) // invalidates iterator on first save! ha!
			for (var i = els.length - 1; i >= 0; i--) {
				var el = els[i];
				suffix = el.id.substr(el.id.length - 5);
				if (suffix == "_save")
					el.click();
			}
		}

		function submit_pip_form(form, step) {
			// save any open fields

			submit_pip_form_data(form);
			reload_pip_form(form, step);
		}

		function clear_pip_form() {
			pip_form_data = {};
		}


		function randomize_cart_id_field() {
			el = document.getElementById("cart_lid");

			c = "bdfghjkmnprstwyz";
			v = "aiueo";

			val = "";
			for (i = 0; i < 5; i++) {
				val = val + c.substr(Math.floor(Math.random() * 16), 1);
				val = val + v.substr(Math.floor(Math.random() * 6), 1);
			}

			el.value = val;
		}

		// https://stackoverflow.com/questions/12710001/how-to-convert-uint8-array-to-base64-encoded-string
		function Uint8ToBase64(u8Arr) {
			var CHUNK_SIZE = 0x8000; //arbitrary number
			var index = 0;
			var length = u8Arr.length;
			var result = '';
			var slice;
			while (index < length) {
				slice = u8Arr.subarray(index, Math.min(index + CHUNK_SIZE, length));
				result += String.fromCharCode.apply(null, slice);
				index += CHUNK_SIZE;
			}
			return btoa(result);
		}


		function reader_onload(evt) {
			// evt.target.result is a ArrayBuffer

			var bytes = new Uint8Array(evt.target.result);
			pip_form_data["file_contents"] = Uint8ToBase64(bytes);

			// show submit button
			var el = document.getElementById("uploaded_button");
			el.style.display = "table";
		}

		function filechooser_onchange(files) {
			var file = files[0];
			var reader = new FileReader();
			reader.onload = function (evt) { reader_onload(evt); };
			reader.readAsArrayBuffer(file);
		}



		function toggle_visible(el) {
			//console.log("@toggle_visible: "+el);

			if (typeof el == 'string') {
				el = document.getElementById(el);
			}

			if (!el) {
				return;
			}

			if (el.style.display == "") {
				el.style.display = "none";
				return false;

			}
			else {

				el.style.display = "";
				return true;
			}
		}

		/*
			show active tab under tab_name, hide others
			assume only one set of tabs on page
		*/
		function show_tab(tab_name, which, tab_group) {
			var el;

			console.log("toggle tab " + tab_name + "_" + which);

			if (!tab_group) tab_group = "form_tab";

			var els = document.getElementsByClassName(tab_group);

			//console.log("found "+els.length);

			for (var i = 0; i < els.length; i++) {
				el = els[i];
				el2 = document.getElementById(el.id + "_content");

				if (el.id == tab_name + "_" + which) {
					// open tab
					el.style["background-color"] = "#f8a";
					el.style.color = "#222";
					if (el2) console.log(" opening " + el2.id);
					if (el2) el2.style.display = "block";
				}
				else {
					// hide tab
					el.style["background-color"] = "";
					el.style.color = "#fff";
					if (el2) console.log(" hiding " + el2.id);
					if (el2) el2.style.display = "none";
				}

			}


		}


		function get_cart_url(lid, cat) {
			//console.log("@get_cart_url lid:"+lid+" cat:"+cat);

			/*
			console.log("@number:  "+Number(lid));
			console.log("@string:  "+String(lid));
			console.log("@equality:"+((Number(lid) == String(lid)) ? "true" : "false"));
			*/

			if (Number(lid) == String(lid) && Number(lid) > 0) {
				num = Math.floor(lid / 10000);
				url = `/bbs/cposts/` + num + `/`;
				if (cat == 6) return url + `cpost` + lid + `.png`;
				if (cat == 7) return url + lid + `.p8.png`;
				if (cat == 8) return url + lid + `.p64.png`;
			}
			else {
				lid = String(lid);
				if (cat == 6) return `/bbs/cposts/` + lid.substr(0, 2) + `/` + lid + `.vx.png`;
				if (cat == 7) return `/bbs/cposts/` + lid.substr(0, 2) + `/` + lid + `.p8.png`;
				if (cat == 8) return `/bbs/cposts/` + lid.substr(0, 2) + `/` + lid + `.p64.png`;
			}
			return '';
		}

		function element_dismisser(el, event) {
			var inner = el.childNodes[1];
			if (!inner) return;
			var rect = inner.getBoundingClientRect();


			if (!el.real_dismiss) {
				//console.log(' @@ element_dismisser: not real click -- return early');
				el.real_dismiss = true; // next time!
				return;
			}

			if (rect && rect.bottom > 0)
				if (event.clientX < rect.left || event.clientX > rect.right ||
					event.clientY > rect.bottom || event.clientY < rect.top) {
					//console.log(' @@ element_dismisser: dismissing');
					console.log(rect);
					el.style.display = "none";
					document.removeEventListener('click', dismisser_func);
				}

		}
		var dismisser_func = null;

		function install_dismisser(el1) {
			//console.log('@@ installing dismisser');

			// just in case
			document.removeEventListener('click', dismisser_func);
			dismisser_func =
				function (event) {
					element_dismisser(el1, event);
				}
			document.addEventListener('click', dismisser_func);
			el1.real_dismiss = false; // hack -- first click is to open pulldown
		}


		function install_search_listener(el) {
			el.addEventListener("keyup",
				function (event) {
					if (event.keyCode == 13)
						window.location.hash = "" + search_url_base + el.value;
				}
			);
		}

		function update_checkout() {
			var checkboxes = document.getElementsByClassName('checkout_checkbox');
			var items = document.getElementsByClassName('checkout_item');

			var show_which = "buy";

			if (document.getElementById("buy_p8") && document.getElementById("buy_p8").checked == true) show_which = show_which + "_p8";
			if (document.getElementById("buy_vx") && document.getElementById("buy_vx").checked == true) show_which = show_which + "_vx";
			if (document.getElementById("buy_p64") && document.getElementById("buy_p64").checked == true) show_which = show_which + "_p64";

			console.log("show_which: " + show_which);

			// show only the selected bundle / product
			for (i = 0; i < items.length; i++) {
				if (items[i].id == show_which)
					items[i].style.display = "";
				else
					items[i].style.display = "none";
			}

		}




		function json_parse(data) {
			if (data[0] != '{') {
				window.alert('Error // Bad Data Format: ' + data);
				return { "printable": "[error]", "status": "bad_data" };
			} return JSON.parse(data);
		}</script>

	<script>

		var cart_info = {};

		// from https://github.com/kripken/emscripten/issues/2053

		function fullscreenable_canvas(canEl) {
			// var canEl = document.getElementById("canvas");

			var reqFuncName = (canEl.mozRequestFullScreen || canEl.webkitRequestFullScreen || { "name": "requestFullScreen" }).name;
			document.addEventListener(reqFuncName.slice(0, -"requestFullScreen".length) + "fullscreenchange", function (evt) {
				if (document.mozFullScreen || document.webkitIsFullScreen || document.fullScreen || document.isFullScreen) {

					var w = canEl.clientWidth;
					var h = canEl.clientHeight;
					var factor = Math.min(screen.width / w, screen.height / h);

					canEl.setAttribute("style", "width: " + Math.round(w * factor) +
						"px !important; height: " + Math.round(h * factor) + "px !important;");

				}
				else {
					canEl.removeAttribute("style");
				}
			}, false);

			// in Firefox full screen must be set to the parent of the canvas to allow resizing the canvas for correct aspect ratio
			if (reqFuncName == "mozRequestFullScreen") {
				canEl[reqFuncName] = function () {
					this.parentNode[reqFuncName].apply(this.parentNode, arguments);
				};
			}
		}


		function pip_requestFullscreen(element) {
			if (element.requestFullscreen) {
				element.requestFullscreen();
			} else if (element.mozRequestFullScreen) {
				element.mozRequestFullScreen();
			} else if (element.webkitRequestFullScreen) {
				element.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
			}
		}


		window.onload = function () {
			//fullscreenable_canvas();
		}



	</script>

	<script>
		function validate_signup(form) {
			if (form.su_agree.checked) {
				return true;
			}
			else {
				alert("If you wish to sign up, you need to agree to the Terms of Use")
				return false;
			}
		}
	</script>
</head>

<body style="background-color:#111;">
	<style type="text/css">
		A:link {
			text-decoration: none;
			color: #937;
			font-weight: bold;
		}

		A:visited {
			text-decoration: none;
			color: #937;
			font-weight: bold;
		}
	</style>
	<script>
		function hide_speech() {
			el = document.getElementById('speech_bubble');
			if (el) el.style.display = "none";

			el = document.getElementById('zoot');
			if (el) el.src = 'https://www.pico-8-edu.com/gfx/zoot_idle.gif';

			//	el = document.getElementById('top_container');
			//	if (el) el.style.maxHeight = "";

		}

		var bunny_timeout_id;


		function bunny_hide_or_say(str, show_back) {
			el = document.getElementById('speech_bubble');

			if (el && el.style.display == "")
				return hide_speech();
			else
				return bunny_say(str, show_back);
		}

		function bunny_say(str, show_back) {
			el = document.getElementById('speech_bubble');

			if (show_back === true) show_back = 'BACK';

			if (el) {
				el.style.display = "";
				str = str + '<div><div style="float:left; display:table; border-radius:5px; margin-top:20px; padding:5px; padding-left:10px; padding-right:10px; background-color:#ffd0c0; cursor:pointer; color:#555; font-size:9pt; border-style:solid; border-width:2px; border-color:#ffa080" onclick="hide_speech();">CLOSE</div>';

				if (show_back)
					str = str + '<div style="float:left; margin-left:15px; display:table; border-radius:5px; margin-top:20px; padding:5px; padding-left:10px; padding-right:10px; background-color:#ffd0c0; cursor:pointer; color:#555;  font-size:9pt;  border-style:solid; border-width:2px; border-color:#ffa080" onclick="bunny_say(help_index);">' + show_back + '</div>';

				el.innerHTML = str + '</div>';
			}

			//	el = document.getElementById('top_container');
			//	if (el) el.style.maxHeight = "45vw";

			el = document.getElementById('zoot');
			if (el) el.src = 'https://www.pico-8-edu.com/gfx/zoot_talking.gif';

			clearTimeout(bunny_timeout_id);

			bunny_timeout_id = setTimeout(function () {
				el = document.getElementById('zoot');
				if (el) el.src = 'https://www.pico-8-edu.com/gfx/zoot_idle.gif';
			}, 4000);

		}


		var help_about = `
Welcome to <span style="font-weight:bold; color:#09f">PICO-8 Education Edition</span>!<br>
<br>

This version includes everything needed to follow tutorials and make cartridges for the <a target="_new" href="https://www.pico-8.com"><span style="font-weight:bold; color:#f49">PICO-8 Fantasy Console</span></a>: an imaginary machine with built-in creation tools.
<br><br>
For the regular version of PICO-8 that runs natively and includes a cartridge browser and exporters, please visit: <br><br>
<a target="_new" href="https://www.pico-8.com">www.pico-8.com</a>

`;

		var showed_touch_message = false;

		var help_start = `

After pressing play &#9655; to boot PICO-8, you can type commands in like this:

<pre>
CIRCFILL(64,64,20,12)
</pre>

Type without shift/capslock and press the Enter key to enter each command.
To try out some demos, use the following commands:

<pre>
INSTALL_DEMOS
CD DEMOS
LOAD HELLO
RUN
</pre>

To stop a running program, press ESC, and ESC once more to jump to the editing tools.
<br><br>
`;

		var help_saving = `

To save a cartridge, type:
<pre>
SAVE MYCART
</pre>
(or whatever you would like to call it). This web version of PICO-8 will download the cartridge each time it is saved for safekeeping.

<br><br>
Cartridges can be loaded by dragging and dropping the .p8 cartridge file into the browser window, or by using the LOAD command.
`;
		/*
		`
		<br><br>
		To save a cartridge in .png format, first capture use .p8.png in the filename:
		<pre>
		SAVE MYCART.P8.PNG
		</pre>
		`;
		*/
		var help_first = `

<p>
Type REBOOT to start a new cartridge, and then ESC for the code editor.
At the top right you can switch between: CODE, GFX, MAP, SFX, and MUSIC.
Try drawing a sprite in slot 1 (the default in the GFX editor), and then type the following program
into the CODE editor:
</p>

<pre>
X = 60
FUNCTION _DRAW()
  CLS(1)
  SPR(1,X,60)
  IF (BTN(0)) X -= 2
  IF (BTN(1)) X += 2
END
</pre>

Then press ESC again to get back to the command prompt, and type RUN to see the program in action.
If everything is working, you can press left and right to control the sprite. Congratulations, you are now a PICO-8 programmer!


`;

		var help_resources = `

Open PICO-8 resources in a new tab:
<br><br>
<a target="_manual" href="https://www.lexaloffle.com/dl/docs/pico-8_manual.html">PICO-8 Manual</a>
<br><br>
<a target="_forum" href="https://www.lexaloffle.com/bbs/?cat=7">PICO-8 Forum & Cartridges</a>
<br><br>
<a target="_resources" href="https://www.lexaloffle.com/pico-8.php?page=resources">Zines, Tutorials & More Resources</a>


`;

		var help_shortcuts = `
<pre>
ESC stop program / toggle editor
CTRL-R run / restart
CTRL-C copy
CTRL-6 save a screenshot
CTRL-8 start recording a gif
CTRL-9 end recording a gif
</pre>

Handy commands:

<pre>
REBOOT to start an empty cartridge
LS     to list the files in the current directory
INSTALL_DEMOS to install some demos
CD     to change directory (e.g. CD DEMOS)
LOAD   to load a cartridge
SAVE   to save a cartridge
</pre>

`;


		var help_index = `

<div style="cursor:pointer; margin-top:15px; font-size:11pt" onclick="bunny_say(help_about, true);"><span style="color:#f8a;font-size:10pt;position:relative;top:-2px; margin-right:6px">&#11044;</span> About PICO-8</div>

<div style="cursor:pointer; margin-top:15px; font-size:11pt" onclick="bunny_say(help_start, true);"><span style="color:#f48;font-size:10pt;position:relative;top:-2px; margin-right:6px">&#11044;</span> Getting Started</div>

<div style="cursor:pointer; margin-top:15px; font-size:11pt" onclick="bunny_say(help_first, true);"><span style="color:#f91;font-size:10pt;position:relative;top:-2px; margin-right:6px">&#11044;</span> First Program</div>

<div style="cursor:pointer; margin-top:15px; font-size:11pt" onclick="bunny_say(help_saving, true);"><span style="color:#4e4;font-size:10pt;position:relative;top:-2px; margin-right:6px">&#11044;</span> Saving Cartridges</div>

<div style="cursor:pointer; margin-top:15px; font-size:11pt" onclick="bunny_say(help_resources, true);"><span style="color:#0af;font-size:10pt;position:relative;top:-2px; margin-right:6px">&#11044;</span> Manual & Tutorials</div>

`;


	</script>

	<div style="width:100%; height:30%; position:absolute; top:0px; left:0px">
		<div id="p8imgholder" style="display:table; min-width:160px; float:right; margin-right:20px; margin-top:80px">
		</div>
	</div>
<!--	<div style="width:100%; position:absolute; left:0px; top:0px">-->
<!--		<div style="font-size:10pt; color:#999; float:right; padding:4px; padding-right:16px;">-->
<!--			// Made by <a target="_twitter" href="https://www.twitter.com/lexaloffle" style="color:#f8a">lexaloffle</a>-->
<!--			&-->
<!--			<a target="_superblog" href="https://www.lexaloffle.com/bbs/superblog.php?cat=7" style="color:#f8a">co</a>-->
<!--		</div>-->
<!--	</div>-->
	<div id="top_container" style="margin-top:32px">
		<script>
			// globals
			var p8_is_running = false;
			var p8_script = null;
			var Module = null;
			var codo_textarea = null;
			var menu_buttons_extra_hack = 0;
			var is_voxatron = false;
			var is_picotron = false;


			var p8_current_playing_lid = null;

			function p8_document() {
				/*
				if (p8_current_playing_lid)
				{
					var playing_div = document.getElementById("cart_player_"+p8_current_playing_lid);
					if (playing_div)
						return playing_div;
				}
				*/

				return document;
			}


			// Default shell for PICO-8 0.1.12

			// options

			// p8_autoplay true to boot the cartridge automatically after page load when possible
			// if the browser can not create an audio context outside of a user gesture (e.g. on iOS), p8_autoplay has no effect
			var p8_autoplay = false;

			// When pico8_state is defined, PICO-8 will set .is_paused, .sound_volume and .frame_number each frame
			// (used for determining button icons)
			var pico8_state = [];

			// use to send keypresses
			//	var codo_key_buffer = [];
			var codo_key_buffer = [];
			var p8_keyboard_state = 0; // mode (toggle with shift)

			// When pico8_buttons is defined, PICO-8 reads each int as a bitfield holding that player's button states
			// 0x1 left, 0x2 right, 0x4 up, 0x8 right, 0x10 O, 0x20 X, 0x40 menu
			// (used by p8_update_gamepads)
			var pico8_buttons = [0, 0, 0, 0, 0, 0, 0, 0]; // max 8 players

			// picotron
			var picotron_buttons = []; // max 8 players

			// When pico8_mouse is defined and .length>0, PICO-8 reads the 3 integers as X, Y and a bitfield for buttons: 0x1 LMB, 0x2 RMB
			var pico8_mouse = [];

			// used to display number of detected joysticks
			var pico8_gamepads = {};
			pico8_gamepads.count = 0;

			// When pico8_gpio is defined, reading and writing to gpio pins will read and write to these values
			var pico8_gpio = new Array(128);

			// When pico8_audio_context context is defined, the html shell (this file) is responsible for creating and managing it
			// Otherwise, PICO-8 will create its own one
			var pico8_audio_context;



			p8_gfx_dat = {
				"p8b_pause1": "",
				"p8b_controls": "",
				"p8b_full": "",
				"p8b_pause0": "",
				"p8b_sound0": "",
				"p8b_sound1": "",
				"p8b_close": "",

				"p8b_cart": '',

				"controls_left_panel": "",


				"controls_right_panel": "",

			};

			// added w/ pico-8 0.2.1: dummys listeners on document required to allow touch events inside iframe (e.g. itch.io player)

			document.addEventListener('touchstart', {});
			document.addEventListener('touchmove', {});
			document.addEventListener('touchend', {});

			// --------------------------------------------------------------------------------------------------------------------------------
			// pico-8 0.2.2: allow dropping files
			var p8_dropped_cart = null;
			var p8_dropped_cart_name = "";
			function p8_drop_file(e) {
				// console.log("@@ dropping file...");

				e.stopPropagation();
				e.preventDefault();

				let file = null;

				// dropped file
				if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0])
					file = e.dataTransfer.files[0]

				// file selected via chooser
				if (!file && e.target && e.target.files && e.target.files[0])
					file = e.target.files[0];


				if (file) {
					// read from file
					reader = new FileReader();

					let dropped_filename = 'dropped.p8.png';
					try {
						if (typeof file.fileName !== 'undefined') dropped_filename = file.fileName;
						if (typeof file.name !== 'undefined') dropped_filename = file.name;
					}
					catch (err) {
						// was happening when inside reader.onload -- files[] becomes an empty set by that stage under Chrome on Chromebook (eh?)
						console.log("@@ failed to set dropped file name: " + err.message + " files:" + JSON.stringify(e.dataTransfer.files));
					}

					console.log("@@ fetching dropped file: " + dropped_filename);
					reader.onload = function (event) {
						p8_dropped_cart = reader.result;
						p8_dropped_cart_name = dropped_filename;
						console.log("@@ finished reading dropped file: " + dropped_filename);

						// data:image/png;base64
						e.stopPropagation();
						e.preventDefault();
						codo_command = 9; // read directly from p8_dropped_cart with libb64 decoder
					};

					reader.readAsDataURL(file);

				}
				else if (e.dataTransfer) {
					// read from url (or data url)
					txt = e.dataTransfer.getData('Text');
					if (txt) {
						p8_dropped_cart_name = "untitled.p8.png";
						p8_dropped_cart = txt;
						codo_command = 9;
					}
				}
			}


			function nop(evt) {
				evt.stopPropagation();
				evt.preventDefault();
			}
			function dragover(evt) {
				evt.stopPropagation();
				evt.preventDefault();
				Module.pico8DragOver();
			}
			function dragstop(evt) {
				evt.stopPropagation();
				evt.preventDefault();
				Module.pico8DragStop();
			}

			// --------------------------------------------------------------------------------------------------------------------------------



			var p8_buttons_hash = -1;
			function p8_update_button_icons() {
				var w = 24;
				var bottom_margin = 12;
				var padding = 4;
				var left = 44;
				var p8tv_mode = false;

				// buttons only appear when running
				if (!p8_is_running) {
					requestAnimationFrame(p8_update_button_icons);
					return;
				}

				// p8tv font page player
				if (typeof (pico8_player_button_w) !== 'undefined') {
					p8tv_mode = true;
					w = Math.floor(pico8_player_button_w);
					// console.log("@@ player_button_w: "+w);
					bottom_margin = Math.floor(w * 3 / 4) - 4;
					padding = Math.floor(w / 4);
					left = Math.floor(w * 2 / 3);
				}

				var is_fullscreen = (document.fullscreenElement || document.mozFullScreenElement || document.webkitIsFullScreen || document.msFullscreenElement);


				// hash based on: pico8_state.sound_volume  pico8_state.is_paused bottom_margin left is_fullscreen p8_touch_detected
				var hash = 0;
				hash = pico8_state.sound_volume;
				if (pico8_state.is_paused) hash += 0x100;
				if (p8_touch_detected) hash += 0x200;
				if (is_fullscreen) hash += 0x400;
				hash += bottom_margin * 0.001;
				hash += left * 1001.3;

				if (p8_buttons_hash == hash) {
					requestAnimationFrame(p8_update_button_icons);
					return;
				}

				p8_buttons_hash = hash;
				// console.log("@@ updating button icons");

				// regenerate every frame (shouldn't be expensive?)
				els = p8_document().getElementsByClassName('p8_menu_button');
				for (i = 0; i < els.length; i++) {
					el = els[i];
					index = el.id;

					if (p8tv_mode) // cludge button positions
					{
						el.style.marginBottom = bottom_margin;
						el.style.paddingBottom = bottom_margin;
						el.style.padding = 0;
						el.style.left = left;
					}
					else {
						// arrrrgh
						//el.style.marginLeft = menu_buttons_extra_hack;
					}

					if (index == 'p8b_sound') index += (pico8_state.sound_volume == 0 ? "0" : "1"); // 1 if undefined
					if (index == 'p8b_pause') index += (pico8_state.is_paused > 0 ? "1" : "0");     // 0 if undefined

					new_str = '<img width=' + w + ' height=' + w + ' style="display:table; pointer-events:none;" src="' + p8_gfx_dat[index] + '">';
					if (el.innerHTML != new_str) // :/
						el.innerHTML = new_str;

					// hide all buttons for touch mode (can pause with menu buttons)

					var is_visible = p8_is_running;

					if (!p8_touch_detected && el.parentElement.id == "p8_menu_buttons_touch")
						//			if (el.parentElement.id == "p8_menu_buttons_touch")
						is_visible = false;

					if (p8_touch_detected && el.parentElement.id == "p8_menu_buttons")
						is_visible = false;

					if (is_fullscreen)
						is_visible = false;

					if (is_visible)
						el.style.display = "";
					else
						el.style.display = "none";
				}
				requestAnimationFrame(p8_update_button_icons);
			}

			function abs(x) {
				return x < 0 ? -x : x;
			}

			function pico8_buttons_event(e, step) {
				if (!p8_is_running) return;

				if (!document.getElementById("touch_controls_gfx")) return;

				// console.log("button event step ",step);

				if (step == 2 && typeof (pico8_mouse) !== 'undefined') {
					pico8_mouse[2] = 0;
				}

				// on canvas

				var num = 0;
				if (e.touches) num = e.touches.length;

				if (num == 0) {
					// no active touches: release mouse button from anywhere on page
					if (typeof (pico8_mouse) !== 'undefined')
						pico8_mouse[2] = 0;
				}
				else {
					let touch = e.touches[0];

					var x = touch.clientX;
					var y = touch.clientY;
					var w = window.innerWidth;
					var h = window.innerHeight;

					let canvas = p8_document().getElementById("canvas");

					if (p8_touch_detected)
						if (typeof (pico8_mouse) !== 'undefined')
							if (canvas) {
								var rect = canvas.getBoundingClientRect();
								//console.log(rect.top, rect.right, rect.bottom, rect.left, x, y);

								if (x >= rect.left && x < rect.right && y >= rect.top && y < rect.bottom) {
									// only define pico8_mouse once it is needed (otherwise codo mouse is clobbered on desktop)

									if (is_picotron)
										pico8_mouse = [
											Math.floor((x - rect.left) * 480 / (rect.right - rect.left)),
											Math.floor((y - rect.top) * 270 / (rect.bottom - rect.top)),
											step < 2 ? 1 : 0
										];
									else
										pico8_mouse = [
											Math.floor((x - rect.left) * 128 / (rect.right - rect.left)),
											Math.floor((y - rect.top) * 128 / (rect.bottom - rect.top)),
											step < 2 ? 1 : 0
										];
									//console.log("-> x y b ", pico8_mouse[0], pico8_mouse[1], pico8_mouse[2]);
									// return; // commented -- blocks overlapping buttons
								} else {
									pico8_mouse[2] = 0;
								}
							}
				}

				if (document.getElementById("touch_controls_gfx").style.display != "none")
					pico8_buttons_event_virtual_dpad(e, step);
				else
					pico8_buttons_event_virtual_keyboard(e, step);
			}

			// ** dupe ** (virtual_dpad)
			function pico8_buttons_event_virtual_keyboard(e, step) {
				if (!p8_is_running) return;
				if (step != 0) return;

				var num = 0;
				if (e.touches) num = e.touches.length;

				for (var i = 0; i < num; i++)
					if (e.touches[i]) {
						var touch = e.touches[i];
						var x = touch.clientX;
						var y = touch.clientY;
						var w = window.innerWidth;
						var h = window.innerHeight;

						var r = Math.min(w, h) / 12;
						if (r > 40) r = 40;
						var keybd_h = (r * 12) * 132.0 / 200.0

						// console.log("x:",x," y:",y," keybd_h:",keybd_h," r:",r);

						if (y < h - r * 9) {
							// no controller buttons up here; includes canvas and menu buttons at top in touch mode
						}
						else {
							e.preventDefault();

							var y1 = Math.floor((y - (h - keybd_h)) * 6 / keybd_h);
							if (y1 == 3) x -= (r * 12.0 / 10.0) * 3.0 / 20.0;
							if (y1 == 4) x -= (r * 12.0 / 10.0) * 6.0 / 20.0;
							var x1 = Math.floor(x * 10 / (r * 12));


							if (x1 >= 0 && x1 < 10 && y1 >= 0 && y1 < 6) {
								// send keypress signal to pico-8

								let key_chars = [
									[
										"X{[(*-=_+X",
										"1234567890",
										"qwertyuiop",
										"asdfghjklX",
										"zxcvbnm,.X",
										"XXXX   <>/"
									],
									[
										"XXXXX[]`~X",
										`!"#$%^&@()`,
										"QWERTYUIOP",
										"ASDFGHJKLX",
										"ZXCVBNM;:X",
										`XXXX   ?'\\\\`
									]
								];

								let val = key_chars[p8_keyboard_state][y1].charCodeAt(x1);
								if ((y1 == 3 || y1 == 4) && x1 == 9) val = 13; // enter

								//if (y1==0 && x1==9) val = 9; // del

								if (y1 == 0 && x1 == 9) val = 8; // backspace
								if (y1 == 0 && x1 == 0) val = 27;
								if (y1 == 5 && x1 >= 0 && x1 < 4) val = -1; // shift, alt, left, right

								codo_key_buffer.push(val);

								// macros
								if (p8_keyboard_state == 0)
									if (y1 == 0 && x1 >= 1 && x1 <= 3) {
										if (x1 == 1) codo_key_buffer.push("}".charCodeAt(0));
										if (x1 == 2) codo_key_buffer.push("]".charCodeAt(0));
										if (x1 == 3) codo_key_buffer.push(")".charCodeAt(0));

										// to do: push left-cursor here
									}

								// special: shift key (show alt keys set)
								if (y1 == 5 && x1 == 0) {
									// toggle

									p8_keyboard_state = p8_keyboard_state ? 0 : 1;

									el = document.getElementById("controls_keyboard_panel");
									if (el)
										el.setAttribute("src", p8_keyboard_state ? "/gfx/controls_keyboard2.png" : "/gfx/controls_keyboard.png");
								}

								p8_give_focus(); // ** hrm.
							}
						}
					}
			}

			// step 0 down 1 drag 2 up (not used)
			function pico8_buttons_event_virtual_dpad(e, step) {
				if (!p8_is_running) return;

				pico8_buttons[0] = 0;

				var num = 0;
				if (e.touches) num = e.touches.length;

				for (var i = 0; i < num; i++)
					if (e.touches[i]) {
						var touch = e.touches[i];
						var x = touch.clientX;
						var y = touch.clientY;
						var w = window.innerWidth;
						var h = window.innerHeight;

						//console.log("dpad touch ",x,y);

						var r = Math.min(w, h) / 12;
						if (r > 40) r = 40;

						b = 0;

						if (y < h - r * 8) {
							// no controller buttons up here; includes canvas and menu buttons at top in touch mode
						}
						else {
							e.preventDefault();

							if ((y < h - r * 6) && y > (h - r * 8)) {
								// menu button: half as high as X O button
								// stretch across right-hand half above X O buttons
								if (x > w - r * 3)
									b |= 0x40;

								// escape button for pwa (doesn't do anything otherwise)
								//关闭掉了左上角触发esc
<!--								if (x < r * 3) {-->
<!--									codo_key_buffer.push(27);-->
<!--									p8_give_focus();-->
<!--								}-->

							}
							else if (x < w / 2 && x < r * 6) {
								// stick

								mask = 0xf; // dpad
								var cx = 0 + r * 3;
								var cy = h - r * 3;

								deadzone = r / 3;
								var dx = x - cx;
								var dy = y - cy;

								if (abs(dx) > abs(dy) * 0.6) // horizontal
								{
									if (dx < -deadzone) b |= 0x1;
									if (dx > deadzone) b |= 0x2;
								}
								if (abs(dy) > abs(dx) * 0.6) // vertical
								{
									if (dy < -deadzone) b |= 0x4;
									if (dy > deadzone) b |= 0x8;
								}
							}
							else if (x > w - r * 6) {
								// button; diagonal split from bottom right corner

								mask = 0x30;

								// one or both of [X], [O]
								if ((h - y) > (w - x) * 0.8) b |= 0x10;
								if ((w - x) > (h - y) * 0.8) b |= 0x20;
							}
						}

						pico8_buttons[0] |= b;

					}

			}


			var p8_update_layout_hash = -1;
			var last_windowed_container_height = 512;
			var last_windowed_container_width = 512;

			function p8_update_layout() {
				var canvas = p8_document().getElementById("canvas");
				var p8_playarea = p8_document().getElementById("p8_playarea");
				var p8_container = p8_document().getElementById("p8_container");
				var p8_frame = p8_document().getElementById("p8_frame");

				var csize = 512;

				var margin_top = 0;
				var margin_left = 0;
				var aspect = p8_aspect;

				// page didn't load yet? first call should be after p8_frame is created
				if (!canvas || !p8_playarea || !p8_container || !p8_frame) {
					p8_update_layout_hash = -1;
					requestAnimationFrame(p8_update_layout);
					return;
				}



				// assumes frame doesn't have padding

				var is_fullscreen = (document.fullscreenElement || document.mozFullScreenElement || document.webkitIsFullScreen || document.msFullscreenElement);
				var frame_width = p8_frame.offsetWidth;
				var frame_height = p8_frame.offsetHeight;

				if (is_fullscreen) {
					// same as window
					frame_width = window.innerWidth;
					frame_height = window.innerHeight;
				}
				else {
					// never larger than window  // (happens when address bar is down in portraight mode on phone)
					frame_width = Math.min(frame_width, window.innerWidth);
					frame_height = Math.min(frame_height, window.innerHeight);
				}

				// as big as will fit in a frame..
				csize = Math.min(frame_width, frame_height);

				// .. but never more than 2/3 of longest side for touch (e.g. leave space for controls on iPad)
				if (p8_touch_detected && p8_is_running) {
					var longest_side = Math.max(window.innerWidth, window.innerHeight);
					csize = Math.min(csize, longest_side * 2 / 3);
				}

				// pixel perfect: quantize to closest multiple of 128
				// only when large display (desktop)
				if (aspect == 1.0) // pico-8
					if (frame_width >= 512 && frame_height >= 512) {
						csize = (csize + 1) & ~0x7f;
					}

				// csize should never be wider / taller than parent frame
				// (otherwise stretched large when fullscreen and then return)
				// update: also -- picotron pushes out width of page and messes up touch controls formatting
				if (!is_fullscreen && p8_frame) {
					// p8_frame_0 parent
					csize = Math.min(csize, last_windowed_container_height);
					csize = Math.min(csize, last_windowed_container_width / aspect);
				}


				if (is_fullscreen) {
					// always center horizontally
					margin_left = (frame_width - (csize * aspect)) / 2;

					if (p8_touch_detected) {
						if (window.innerWidth < window.innerHeight) {
							// portrait: keep at y=40 (avoid rounded top corners / camer num thing etc.)
							margin_top = Math.min(40, frame_height - csize);
						}
						else {
							// landscape: put a little above vertical center
							margin_top = (frame_height - csize) / 4;
						}
					}
					else {
						// non-touch: center vertically
						margin_top = (frame_height - csize) / 2;
					}

					// turn off temp hack
					/* deleteme
								canvas.style.position = "";
								canvas.style.left = 0.0;
					*/

				}
				/* deleteme
						else
						{

							// temp hack: had to remove margin:auto from p8_container tostop blurry scale under chrome
							p8_container.style.margin = 0; // WHHHHY
							let left = (frame_width - (csize * aspect))/2;
							canvas.style.position = "relative";
							canvas.style.left = left;
							menu_buttons_extra_hack = left;

				//			p8_menu_buttons.style.marginLeft = 40;//10.0 + Math.floor(left);

						}
				*/

				// temporary voxatron hacks
				//		if (p8_aspect > 1.0)
				//		 	margin_left -= 40;


				// skip if relevant state has not changed

				var update_hash = csize + margin_top * 1000.3 + margin_left * 0.001 + frame_width * 333.33 + frame_height * 772.15134;
				if (is_fullscreen) update_hash += 0.1237;

				if (!is_fullscreen) // fullscreen: update every frame for safety. should be cheap!
					if (!p8_touch_detected) // mobile: update every frame because nothing can be trusted
						if (p8_update_layout_hash == update_hash) {
							//console.log("p8_update_layout(): skipping");
							requestAnimationFrame(p8_update_layout);
							return;
						}
				p8_update_layout_hash = update_hash;

				// record this for returning to original size after fullscreen pushes out container height (argh)
				if (!is_fullscreen && p8_frame) {
					last_windowed_container_height = p8_frame.parentNode.parentNode.offsetHeight;
					last_windowed_container_width = p8_frame.parentNode.parentNode.offsetWidth;
				}


				//console.log("@@ p8_update_layout(): updating "+(is_fullscreen ? "fullscreen" : "windowed")+"  csize: " + csize);

				// mobile in portrait mode: put screen at top (w / a little space for fullscreen button)
				// (don't cart about buttons overlapping screen)
				if (p8_touch_detected && p8_is_running && document.body.clientWidth < document.body.clientHeight)
					p8_playarea.style.marginTop = 32;
				else if (p8_touch_detected && p8_is_running) // landscape: slightly above vertical center (only relevant for iPad / highres devices)
					p8_playarea.style.marginTop = (document.body.clientHeight - csize) / 4;
				else
					p8_playarea.style.marginTop = "";

				canvas.style.width = csize * aspect;
				canvas.style.height = csize;

				// to do: this should just happen from css layout. used in fullscreen
				canvas.style.marginLeft = margin_left;
				canvas.style.marginTop = margin_top;

				// console.log("margin_left: "+margin_left+" width: "+(csize * aspect));

				p8_container.style.width = csize * aspect;
				p8_container.style.height = csize;

				if (p8_touch_detected && p8_is_running) {
					// turn off pointer events to prevent double-tap zoom etc (works on Android)
					// don't want this for desktop because breaks mouse input & click-to-focus when using codo_textarea
					canvas.style.pointerEvents = "none";

					p8_container.style.marginTop = "0px";

					// buttons

					// same as touch event handling
					var w = window.innerWidth;
					var h = window.innerHeight;
					var r = Math.min(w, h) / 12;

					if (r > 40) r = 40;

					el = document.getElementById("controls_right_panel");
					el.style.left = w - r * 6;
					el.style.top = h - r * 7;
					el.style.width = r * 6;
					el.style.height = r * 7;
					if (el.getAttribute("src") != p8_gfx_dat["controls_right_panel"]) // optimisation: avoid reload? (browser should handle though)
						el.setAttribute("src", p8_gfx_dat["controls_right_panel"]);

					el = document.getElementById("controls_left_panel");
					el.style.left = 0;
					el.style.top = h - r * 6;
					el.style.width = r * 6;
					el.style.height = r * 6;
					if (el.getAttribute("src") != p8_gfx_dat["controls_left_panel"]) // optimisation: avoid reload? (browser should handle though)
						el.setAttribute("src", p8_gfx_dat["controls_left_panel"]);

					el = document.getElementById("controls_keyboard_panel");
					el.style.left = 0;
					el.style.top = h - r * 12 * (132.0 / 200.0);
					el.style.width = r * 12;
					el.style.height = r * 12 * (132.0 / 200.0);

					if (el.getAttribute("src") == "")
						el.setAttribute("src", "/gfx/controls_keyboard.png");

					// scroll to cart (need to stop running with X)
					p8_frame.scrollIntoView(true);

					if (pico8_state.show_dpad == 0 && w < h) // not true when undefined
					{
						// virtual keyboard
						document.getElementById("touch_controls_gfx").style.display = "none";
						document.getElementById("touch_keyboard_gfx").style.display = "table";

						// hide touch menu bottons
						//document.getElementById("p8_menu_buttons_touch").style.display="none";

					}
					else {
						// virtual dpad
						document.getElementById("touch_controls_gfx").style.display = "table";
						document.getElementById("touch_keyboard_gfx").style.display = "none";
					}


					// don't use background -- just hide body div
					//document.getElementById("touch_controls_background").style.display="table";

				}
				else {
					// no touch
					document.getElementById("touch_controls_gfx").style.display = "none";
					document.getElementById("touch_keyboard_gfx").style.display = "none";

					//document.getElementById("touch_controls_background").style.display="none";
				}

				if (!p8_is_running) {
					p8_playarea.style.display = "none";
					p8_container.style.display = "flex";
					p8_container.style.marginTop = "auto";

					el = p8_document().getElementById("p8_start_button");
					if (el) el.style.display = "flex";
				}

				requestAnimationFrame(p8_update_layout);
			}


			//注释：虚拟手柄定义，之前默认关闭，现在修改为默认开启
			var p8_touch_detected = true;

			//addEventListener("click", function(event){alert(pico8_state.show_dpad);});

			addEventListener("touchstart", function (event) {
				p8_touch_detected = true;

				// hide textarea, so that virtual mobile keyboard doesn't come up
				// (and fall back to internal copy/paste -- can't paste from other apps, but can ctrl-c,v within PICO-8)
				if (codo_textarea && codo_textarea.style.display != "none")
					codo_textarea.style.display = "none";

				/* // deleteme
						if (typeof(pico8_state.show_dpad) === 'undefined' || pico8_state.show_dpad)
						{
							if (codo_textarea && codo_textarea.style.display != "none")
								codo_textarea.style.display="none";
						}
						else
						{
							if (codo_textarea && codo_textarea.style.display != "")
								codo_textarea.style.display="";
						}
				*/


			}, { passive: true });

			function p8_create_audio_context() {
				// console.log("p8_create_audio_context()");
				if (pico8_audio_context) {
					try {
						pico8_audio_context.resume();
					}
					catch (err) {
						console.log("** pico8_audio_context.resume() failed");
					}
					return;
				}

				var webAudioAPI = window.AudioContext || window.webkitAudioContext || window.mozAudioContext || window.oAudioContext || window.msAudioContext;
				if (webAudioAPI) {
					pico8_audio_context = new webAudioAPI;

					// wake up iOS
					if (pico8_audio_context) {
						try {
							var dummy_source_sfx = pico8_audio_context.createBufferSource();
							dummy_source_sfx.buffer = pico8_audio_context.createBuffer(1, 1, 22050); // dummy
							dummy_source_sfx.connect(pico8_audio_context.destination);
							dummy_source_sfx.start(1, 0.25); // gives InvalidStateError -- why? hasn't been played before
							//dummy_source_sfx.noteOn(0); // deleteme
						}
						catch (err) {
							console.log("** dummy_source_sfx.start(1, 0.25) failed");
						}
					}
				}
			}


			// just hides. can reopen in a paused state.
			// used only by mobile X button after touch_detected
			function p8_close_cart() {
				p8_is_running = false;
				p8_touch_detected = false;
				Module.pico8SetPaused(1);

				// hide stuff

				el = document.getElementById("p8_frame_0");
				if (el) el.style.display = "none";

				// show page
				el = document.getElementById("body_0");
				if (el) el.style.display = "";
				el = document.getElementById("lex_footer");
				if (el) el.style.display = "";

				// (re-)show dormant players
				els = document.getElementsByClassName("dormant_player");
				for (i = 0; i < els.length; i++)
					els[i].style.display = ''; // show

			}


			function p8_run_cart(player_url, cart_lid, cart_url) {
				console.log("p8_run_cart: " + player_url + " " + cart_lid + " " + cart_url);
				p8_current_playing_lid = cart_lid;
				//codo_textarea = document.getElementById("codo_textarea_global");
				codo_textarea = document.getElementById("codo_textarea_" + cart_lid);

				// e.g. for "currently playing" update
				if (typeof (p8_run_cart_onrun) !== 'undefined') {
					p8_run_cart_onrun(cart_lid);
				}

				if (p8_is_running) {
					if (cart_lid != 0) {
						_cartname = [String(cart_lid)];
						codo_command = 6;
					}
					return;
				}

				p8_is_running = true;

				// create audio context and wake it up (for iOS -- needs happen inside touch event)
				p8_create_audio_context();

				// show touch elements
				els = document.getElementsByClassName('p8_controller_area');
				for (i = 0; i < els.length; i++)
					els[i].style.display = "";


				// install touch events. These also serve to block scrolling / pinching / zooming on phones when p8_is_running
				// moved event.preventDefault(); calls into pico8_buttons_event (want to let top buttons pass through)
				addEventListener("touchstart", function (event) { pico8_buttons_event(event, 0); }, { passive: false });
				addEventListener("touchmove", function (event) { pico8_buttons_event(event, 1); }, { passive: false });
				addEventListener("touchend", function (event) { pico8_buttons_event(event, 2); }, { passive: false });


				// load and run script
				e = document.createElement("script");
				p8_script = e;
				e.onload = function () {

					//window.alert("loaded "+p8_update_layout_hash);

					// show canvas / menu buttons only after loading
					el = document.getElementById("p8_playarea");
					if (el) el.style.display = "table";

					if (typeof (last_pico8_player_layout_hash) !== 'undefined') // p8tv
						last_pico8_player_layout_hash = -1;
					if (typeof (p8_update_layout_hash) !== 'undefined')
						p8_update_layout_hash = -77;
					if (typeof (p8_buttons_hash) !== 'undefined')
						p8_buttons_hash = -1;

					/*
								// happens outside; when generating player.
								Module = {};
								Module.canvas = document.getElementById("canvas");
								Module.arguments = [cart_url.toString()];
								p8_update_button_icons();
					*/

					// Module.arguments = [cart_url.toString()]; // doesn't work
					// hack: use command 6 instead. will load as soon as codo_update is spinning
					// allows starting player AND choosing cart by clicking on p8tv cart
					// could just always do this anyway; is cleaner.

					if (cart_lid != 0) {
						_cartname = [String(cart_lid)];
						codo_command = 6;
					}

					// Check if we have a cart URL to load
					if (typeof window.p8_pending_cart_url !== 'undefined' && window.p8_pending_cart_url) {
						console.log("Loading pending cart:", window.p8_pending_cart_url);
						const autoRun = typeof window.p8_pending_auto_run !== 'undefined' ? window.p8_pending_auto_run : true;
						p8_load_cart_from_url(window.p8_pending_cart_url, autoRun);
						window.p8_pending_cart_url = null; // Clear it
						window.p8_pending_auto_run = null; // Clear it
					}

					// install drag and drop thing

					function noopHandler(evt) {
						evt.stopPropagation();
						evt.preventDefault();
					}
					var canvas = p8_document().getElementById("canvas");

					canvas.addEventListener('dragenter', noopHandler, false);
					canvas.addEventListener('dragover', noopHandler, false);
					canvas.addEventListener('dragleave', noopHandler, false);
					canvas.addEventListener('drop', noopHandler, false);

					canvas.addEventListener('drop', p8_drop_file, false);

				}
				e.type = "application/javascript";
				e.src = player_url;
				e.id = "e_script";

				document.body.appendChild(e); // load and run

				// hide start button and show canvas / menu buttons. hide start button
				el = document.getElementById("p8_start_button");
				if (el) el.style.display = "none";

				// add #playing for touchscreen devices (allows back button to close)
				if (p8_touch_detected) {
					window.location.hash = "#playing";
					window.onhashchange = function () {
						if (window.location.hash.search("playing") < 0)
							p8_close_cart();
					}
				}

				// install drag&drop listeners
				{
					let canvas = p8_document().getElementById("canvas");
					if (canvas) {
						canvas.addEventListener('dragenter', dragover, false);
						canvas.addEventListener('dragover', dragover, false);
						canvas.addEventListener('dragleave', dragstop, false);
						canvas.addEventListener('drop', nop, false);
						canvas.addEventListener('drop', p8_drop_file, false);
					}
				}

				// install "sure you'd like to navigate away?" thing
				window.onbeforeunload = function () {
					if (pico8_state.require_page_navigate_confirmation)
						return "Are you sure you want to navigate away?";
					else
						return null; // ok to close immediately
				}

			}

			// Gamepad code  // from @weeble's mod

			var P8_BUTTON_O = { action: 'button', code: 0x10 };
			var P8_BUTTON_X = { action: 'button', code: 0x20 };
			var P8_DPAD_LEFT = { action: 'button', code: 0x1 };
			var P8_DPAD_RIGHT = { action: 'button', code: 0x2 };
			var P8_DPAD_UP = { action: 'button', code: 0x4 };
			var P8_DPAD_DOWN = { action: 'button', code: 0x8 };
			var P8_MENU = { action: 'menu' };
			var P8_NO_ACTION = { action: 'none' };

			var P8_BUTTON_MAPPING = [
				// ref: https://w3c.github.io/gamepad/#remapping
				P8_BUTTON_O,          // Bottom face button
				P8_BUTTON_X,          // Right face button
				P8_BUTTON_X,          // Left face button
				P8_BUTTON_O,          // Top face button
				P8_NO_ACTION,         // Near left shoulder button (L1)
				P8_NO_ACTION,         // Near right shoulder button (R1)
				P8_NO_ACTION,         // Far left shoulder button (L2)
				P8_NO_ACTION,         // Far right shoulder button (R2)
				P8_MENU,              // Left auxiliary button (select)
				P8_MENU,              // Right auxiliary button (start)
				P8_NO_ACTION,         // Left stick button
				P8_NO_ACTION,         // Right stick button
				P8_DPAD_UP,           // Dpad up
				P8_DPAD_DOWN,         // Dpad down
				P8_DPAD_LEFT,         // Dpad left
				P8_DPAD_RIGHT,        // Dpad right
			];

			// Track which player is controller by each gamepad. Gamepad index i controls the
			// player with index pico8_gamepads_mapping[i]. Gamepads with null player are
			// currently unassigned - they get assigned to a player when a button is pressed.
			var pico8_gamepads_mapping = [];

			function p8_unassign_gamepad(gamepad_index) {
				if (pico8_gamepads_mapping[gamepad_index] == null) {
					return;
				}
				pico8_buttons[pico8_gamepads_mapping[gamepad_index]] = 0;
				pico8_gamepads_mapping[gamepad_index] = null;
			}


			function p8_first_player_without_gamepad(max_players) {
				var allocated_players = pico8_gamepads_mapping.filter(function (x) { return x != null; });
				var sorted_players = Array.from(allocated_players).sort();
				for (var desired = 0; desired < sorted_players.length && desired < max_players; ++desired) {
					if (desired != sorted_players[desired]) {
						return desired;
					}
				}
				if (sorted_players.length < max_players) {
					return sorted_players.length;
				}
				return null;
			}

			function p8_assign_gamepad_to_player(gamepad_index, player_index) {
				p8_unassign_gamepad(gamepad_index);
				pico8_gamepads_mapping[gamepad_index] = player_index;
			}

			function p8_convert_standard_gamepad_to_button_state(gamepad, axis_threshold, button_threshold) {
				// Given a gamepad object, return:
				// {
				//     button_state: the binary encoded Pico 8 button state
				//     menu_button: true if any menu-mapped button was pressed
				//     any_button: true if any button was pressed, including d-pad
				//         buttons and unmapped buttons
				// }
				if (!gamepad || !gamepad.axes || !gamepad.buttons) {
					return {
						button_state: 0,
						menu_button: false,
						any_button: false,
						picotron_button_state: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
					};
				}
				function button_state_from_axis(axis, low_state, high_state, default_state) {
					if (axis && axis < -axis_threshold) return low_state;
					if (axis && axis > axis_threshold) return high_state;
					return default_state;
				}
				var axes_actions = [
					button_state_from_axis(gamepad.axes[0], P8_DPAD_LEFT, P8_DPAD_RIGHT, P8_NO_ACTION),
					button_state_from_axis(gamepad.axes[1], P8_DPAD_UP, P8_DPAD_DOWN, P8_NO_ACTION),
				];

				var button_actions = gamepad.buttons.map(function (button, index) {
					var pressed = button.value > button_threshold || button.pressed;
					if (!pressed) return P8_NO_ACTION;
					return P8_BUTTON_MAPPING[index] || P8_NO_ACTION;
				});

				var all_actions = axes_actions.concat(button_actions);

				var menu_button = button_actions.some(function (action) { return action.action == 'menu'; });
				var button_state = (all_actions
					.filter(function (a) { return a.action == 'button'; })
					.map(function (a) { return a.code; })
					.reduce(function (result, code) { return result | code; }, 0)
				);
				var any_button = gamepad.buttons.some(function (button) {
					return button.value > button_threshold || button.pressed;
				});

				any_button |= button_state; //jww: include axes 0,1 as might be first intended action

				// picotron

				var picotron_button_state = [
					-gamepad.axes[0], gamepad.axes[0], -gamepad.axes[1], gamepad.axes[1],
					gamepad.buttons[1].value, gamepad.buttons[0].value,
					gamepad.buttons.length > 9 ? Math.max(gamepad.buttons[8].value, gamepad.buttons[9].value) : 0, // menu button
					0, // 7: reserved

					-gamepad.axes[2], gamepad.axes[2], -gamepad.axes[3], gamepad.axes[3], // secondary stick
					gamepad.buttons[2].value, // diamond-L
					gamepad.buttons[3].value, // diamond-U
					gamepad.buttons[4].value, gamepad.buttons[5].value  // shoulder L, R
				];

				// dpad
				if (gamepad.buttons.length >= 16) {
					picotron_button_state[0] = Math.max(picotron_button_state[0], gamepad.buttons[14].value);
					picotron_button_state[1] = Math.max(picotron_button_state[1], gamepad.buttons[15].value);
					picotron_button_state[2] = Math.max(picotron_button_state[2], gamepad.buttons[12].value);
					picotron_button_state[3] = Math.max(picotron_button_state[3], gamepad.buttons[13].value);
				}

				// extra menu button: big one in the middle
				if (gamepad.buttons.length >= 17)
					picotron_button_state[6] = Math.max(picotron_button_state[6], gamepad.buttons[16].value);

				// picotron expects integer values 0..32767
				for (var j = 0; j < 16; j++) {
					picotron_button_state[j] = Math.max(0, Math.min(Math.floor(picotron_button_state[j] * 32767.0), 32767));
					if (picotron_button_state[j] >= 8192) {
						any_button |= 0xff;
						//console.log("btn "+j+": "+picotron_button_state[j]);
					}
				}

				return {
					button_state,
					menu_button,
					any_button,
					picotron_button_state
				};
			}

			// jww: pico-8 0.2.1 version for unmapped gamepads, following p8_convert_standard_gamepad_to_button_state
			// axes 0,1 & buttons 0,1,2,3 are reasonably safe. don't try to read dpad.
			// menu buttons are unpredictable, but use 6..8 anyway (better to have a weird menu button than none)
			function p8_convert_unmapped_gamepad_to_button_state(gamepad, axis_threshold, button_threshold) {

				if (!gamepad || !gamepad.axes || !gamepad.buttons) {
					return {
						button_state: 0,
						menu_button: false,
						any_button: false,
						picotron_button_state: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
					};
				}

				var button_state = 0;

				if (gamepad.axes[0] && gamepad.axes[0] < -axis_threshold) button_state |= 0x1;
				if (gamepad.axes[0] && gamepad.axes[0] > axis_threshold) button_state |= 0x2;
				if (gamepad.axes[1] && gamepad.axes[1] < -axis_threshold) button_state |= 0x4;
				if (gamepad.axes[1] && gamepad.axes[1] > axis_threshold) button_state |= 0x8;

				// buttons: first 4 taken to be O/X, 6..8 taken to be menu button

				for (j = 0; j < gamepad.buttons.length; j++)
					if (gamepad.buttons[j].value > 0 || gamepad.buttons[j].pressed) {
						if (j < 4)
							button_state |= (0x10 << (((j + 1) / 2) & 1)); // 0 1 1 0 -- A,X -> O,X on xbox360
						else if (j >= 6 && j <= 8)
							button_state |= 0x40;
					}

				var menu_button = button_state & 0x40;


				var any_button = gamepad.buttons.some(function (button) {
					return button.value > button_threshold || button.pressed;
				});

				any_button |= button_state; //jww: include axes 0,1 as might be first intended action

				// picotron: use pico-8
				var picotron_button_state = [
					(button_state & 1) ? 0x7fff : 0,
					(button_state & 2) ? 0x7fff : 0,
					(button_state & 4) ? 0x7fff : 0,
					(button_state & 8) ? 0x7fff : 0,
					(button_state & 16) ? 0x7fff : 0,
					(button_state & 32) ? 0x7fff : 0,
					(button_state & 64) ? 0x7fff : 0,
					0,
					0, 0, 0, 0, 0, 0, 0, 0
				];


				return {
					button_state,
					menu_button,
					any_button,
					picotron_button_state
				};
			}

			// gamepad  https://developer.mozilla.org/en-US/docs/Web/API/Gamepad_API/Using_the_Gamepad_API
			// (sets bits in pico8_buttons[])
			function p8_update_gamepads() {
				var axis_threshold = 0.3;
				var button_threshold = 0.5; // Should be unnecessary, we should be able to trust .pressed
				var max_players = 8;
				var gps = navigator.getGamepads() || navigator.webkitGetGamepads();

				if (!gps) return;

				// In Chrome, gps is iterable but it's not an array.
				gps = Array.from(gps);

				pico8_gamepads.count = gps.length;
				while (gps.length > pico8_gamepads_mapping.length) {
					pico8_gamepads_mapping.push(null);
				}

				var menu_button = false;
				var gamepad_states = gps.map(function (gp) {
					return (gp && gp.mapping == "standard") ?
						p8_convert_standard_gamepad_to_button_state(gp, axis_threshold, button_threshold) :
						p8_convert_unmapped_gamepad_to_button_state(gp, axis_threshold, button_threshold);
				});

				// Unassign disconnected gamepads.
				// gps.forEach(function (gp, i) { if (gp && !gp.connected) { p8_unassign_gamepad(i); }});
				gps.forEach(function (gp, i) { if (!gp || !gp.connected) { p8_unassign_gamepad(i); } }); // https://www.lexaloffle.com/bbs/?pid=87132#p

				// Assign unassigned gamepads when any button is pressed.
				gamepad_states.forEach(function (state, i) {
					if (state.any_button && pico8_gamepads_mapping[i] == null) {
						var first_free_player = p8_first_player_without_gamepad(max_players);
						p8_assign_gamepad_to_player(i, first_free_player);
					}
				});

				// Update pico8_buttons array.
				gamepad_states.forEach(function (gamepad_state, i) {
					if (pico8_gamepads_mapping[i] != null) {
						pico8_buttons[pico8_gamepads_mapping[i]] = gamepad_state.button_state;
						picotron_buttons[pico8_gamepads_mapping[i]] = gamepad_state.picotron_button_state;
					}
				});

				// Update menu button.
				// Pico 8 only recognises the menu button on the first player, so we
				// press it when any gamepad has pressed a button mapped to menu.
				if (gamepad_states.some(function (state) { return state.menu_button; })) {
					pico8_buttons[0] |= 0x40;
					if (picotron_buttons[0]) picotron_buttons[0][6] = 0xff; // ditto for picotron
				}

				requestAnimationFrame(p8_update_gamepads);
			}
			requestAnimationFrame(p8_update_gamepads);

			// End of gamepad code


			// key blocker. prevent cursor keys from scrolling page while playing cart.
			// also don't act on M, R so that can mute / reset cart
			document.addEventListener('keydown',
				function (event) {
					event = event || window.event;
					if (!p8_is_running) return;

					if (pico8_state.has_focus == 1)
						if ([17, 88, 67, 86].indexOf(event.keyCode) < 0) // block all keypresses except ctrl,x,c,v (need for codo_textfield clipboard)
							if (event.preventDefault) event.preventDefault();

					// extra keys to block from picotron shell
					// block: cursors(37..40), M (77) R (82) P (80), tab (9)  pageup/down(33,34)  slash (191)
					if (is_picotron)
						if (pico8_state.has_focus == 1)
							if ([32, 37, 38, 39, 40, 77, 82, 80, 9, 33, 34, 191].indexOf(event.keyCode) > -1)
								if (event.preventDefault) event.preventDefault();

				}, { passive: false });

			// same as in codo_update_js_textfield()
			function p8_give_focus() {
				el = (typeof codo_textarea === 'undefined') ? document.getElementById("codo_textarea") : codo_textarea;
				if (el) {
					el.focus();
					el.select();
				}
			}

			function p8_request_fullscreen() {

				var is_fullscreen = (document.fullscreenElement || document.mozFullScreenElement || document.webkitIsFullScreen || document.msFullscreenElement);

				if (is_fullscreen) {
					if (document.exitFullscreen) {
						document.exitFullscreen();
					} else if (document.webkitExitFullscreen) {
						document.webkitExitFullscreen();
					} else if (document.mozCancelFullScreen) {
						document.mozCancelFullScreen();
					} else if (document.msExitFullscreen) {
						document.msExitFullscreen();
					}
					return;
				}

				var el = document.getElementById("p8_playarea");

				if (el.requestFullscreen) {
					el.requestFullscreen();
				} else if (el.mozRequestFullScreen) {
					el.mozRequestFullScreen();
				} else if (el.webkitRequestFullScreen) {
					el.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
				}
			}


			var p8_aspect = 1.0;

			function activate_p8_player(player_url, cart_lid, cart_url, new_parent_id, dormant_player_id) {
				var p8_frame_0 = document.getElementById("p8_frame_0");
				var new_parent = document.getElementById(new_parent_id);
				var dormant_player = document.getElementById(dormant_player_id);

				p8_aspect = 1.0;
				if (player_url.indexOf("vox") >= 0) is_voxatron = true;
				if (player_url.indexOf("picotron") >= 0 || player_url.indexOf("playground") >= 0) is_picotron = true;


				if (is_voxatron) p8_aspect = 820.0 / 512.0;
				if (is_picotron) p8_aspect = 480.0 / 270.0;


				if (!p8_frame_0) {
					console.log("@@ could not find p8_frame_0");
					return;
				}

				p8_frame_0.parentNode.removeChild(p8_frame_0);
				new_parent.appendChild(p8_frame_0);

				//p8_frame_0.style.display="table";
				p8_frame_0.style = "display:table; width:100%;height:100%; max-width:100vw;max-height:100vh;  min-width:256px;min-height:256px;margin:0px;background-color:#111"

				dormant_player.style.display = 'none';

				// bbs player: can remove entire page and move player div to front
				// (differs from exported player approach

				if (p8_touch_detected) {
					el = document.getElementById("body_0");
					if (el) el.style.display = "none";
					el.parentNode.appendChild(document.getElementById("p8_frame_0"));

					el = document.getElementById("lex_footer");
					if (el) el.style.display = "none";
				}

				// run!
				p8_run_cart(player_url, cart_lid, cart_url);

				// load cart menu
				embedded = (window.parent && window.parent != window) ? 1 : 0;

				// to do: decide cab (and make cab system clearer. count as bbs play -- need for voxatron embeds)

				//var el = document.getElementById("more_carts_global");
				//if (!el) console.log("@@ could not find "+"more_carts_"+cart_lid);
				//if (el) // only load if can find element for it
				{
					microAjax("/bbs/on_play.php?id=" + cart_lid + "&embedded=" + embedded + "&cab=0",
						function (retdata) {
							var el = document.getElementById("more_carts_" + cart_lid);
							//var el = document.getElementById("more_carts_global");
							if (el) el.innerHTML = retdata;
						}
					);
				}

				// show dormant frames on other carts // was set_active_widget()
				els = document.getElementsByClassName("dormant_player");
				for (i = 0; i < els.length; i++)
					if (els[i] != dormant_player)
						els[i].style.display = ''; // show


				// safety. should be unnecessary :|
				// when running wasm version, canvas seems right size, but then shrinks. sdl? to do: investigate
				setTimeout(function () {
					p8_update_layout_hash = -56;
				}, 200);
				setTimeout(function () {
					p8_update_layout_hash = -56;
				}, 300);
				setTimeout(function () {
					p8_update_layout_hash = -57;
				}, 500);
				setTimeout(function () {
					p8_update_layout_hash = -58;
				}, 1000);
				setTimeout(function () {
					p8_update_layout_hash = -59;
				}, 2000);

			}

			function toggle_cart_menu(div_id) {
				var el = $(div_id);
				if (el) {
					el.style.display = (el.style.display == 'none') ? 'table' : 'none';
					var slider_el = el.childNodes[1];

					if (true) {
						left_target = 0;
						slider_el.style.left = (left_target + 540) + 'px';

						poll_function(200, 10,
							function (q) {
								q = 1 - (1 - q) * (1 - q);
								slider_el.style.left = (left_target + (1 - q) * 540) + 'px';
							});
					}
					codo_running = (el.style.display == 'none'); // running if hidden
					codo_command = 5; codo_command_p = !codo_running;
				}
			}

			// for downloading saved carts
			function download_browser_file(filename, contents) {
				var element = document.createElement('a');
				if (filename.substr(filename.length - 7) == ".p8.png")
					element.setAttribute('href', 'data:image/png;base64,' + encodeURIComponent(contents));
				else if (filename.substr(filename.length - 4) == ".wav")
					element.setAttribute('href', 'data:audio/x-wav;base64,' + encodeURIComponent(contents));
				else
					element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(contents));
				element.setAttribute('download', filename);
				element.style.display = 'none';
				document.body.appendChild(element);
				element.click();
				document.body.removeChild(element);
			}

		</script>
		<STYLE TYPE="text/css">
			<!--.p8_menu_button{
			opacity:0.2;
			padding:4px;
			display:table;
			width:24px;
			height:24px;
			}


			.p8_menu_button:hover {
				opacity: 1.0;
				cursor: pointer;
			}

			canvas {
				image-rendering: optimizeSpeed;
				image-rendering: -moz-crisp-edges;
				image-rendering: -webkit-optimize-contrast;
				image-rendering: optimize-contrast;
				image-rendering: pixelated;
				-ms-interpolation-mode: nearest-neighbor;
				border: 0px;
				cursor: none;
			}


			.button_gfx {
				stroke-width: 2;
				stroke: #ffffff;
				stroke-opacity: 0.4;
				fill-opacity: 0.2;
				fill: black;
			}

			.button_gfx_icon {
				stroke-width: 3;
				stroke: #909090;
				stroke-opacity: 0.7;
				fill: none;
			}

			.p8_start_button {
				cursor: pointer;
				background-repeat: center;
				-webkit-background-size: cover;
				-moz-background-size: cover;
				-o-background-size: cover;
				background-size: cover;
			}

			.p8_menu_buttons {
				display: flex;
				flex-direction: column;
				height: 512px;
				background-color: #f8a;
			}

			@media screen and (max-width: 800px) {
				.cart_player_wrapper {
					width: 384px;
					height: 384px;
				}

				.p8_menu_buttons {
					margin-top: 0px;
					display: none;
				}
			}
			-->
		</STYLE>
		<div id="p8_frame_0"
			style="display:block; width:100%;height:100%; max-width:95vw;max-height:95vh; min-width:256px;min-height:256px;margin:auto;background-color:#111">
			<div id="p8_frame"
				style="display:flex; width:100%;height:100%; min-width:256px;min-height:256px;margin:auto;">
				<textarea id="codo_textarea_0" class="emscripten"
					style="position:absolute; left:-9999px; height:0px; overflow:hidden"></textarea><input
					id="p8_file_chooser" type="file" name="name" style="display: none;"
					onchange='p8_drop_file(event)' />
				<div id="p8_container" style="margin:auto; display:table;"
					onclick="if (!p8_is_running) {p8_create_audio_context(); p8_run_cart('./js/pico8_edu_0206c_dev8.js', '0', '/bbs/cposts/bad_lid_0_.png');}">


					<div id="p8_start_button" class="p8_start_button" style="width:100%; height:100%; display:flex; cursor:pointer; background:url('');
                    background-repeat: center;
                        -webkit-background-size:cover; -moz-background-size:cover; -o-background-size:cover; background-size:cover;
                    ">
						<img width=80 height=80 style="margin:auto;"
							src="" />

					</div>



					<div id="p8_playarea" style="display:none; margin:auto;
            -webkit-user-select:none; -moz-user-select: none; user-select: none; -webkit-touch-callout:none;
        ">

						<div id="touch_controls_background" style=" pointer-events:none; display:none; background-color:#000; opacity:0.5;
                         position:fixed; top:0px; left:0px; border:0; width:100vw; height:200vh; overflow:hidden">
							&nbsp
						</div>


						<div id="p8_playarea_flex" style="display:flex; position:relative">

							<canvas class="emscripten" id="canvas" oncontextmenu="event.preventDefault();">
							</canvas>


						</div>

						<!-- display after first layout update -->

						<div id="touch_controls_gfx" style=" pointer-events:none; display:table;
                         position:fixed; top:0px; left:0px; border:0; width:100vw; height:100vh">

							<img src="" id="controls_right_panel" style="position:absolute; opacity:0.5;">
							<img src="" id="controls_left_panel" style="position:absolute;  opacity:0.5;">

						</div> <!-- touch_controls_gfx -->

						<div id="touch_keyboard_gfx" style=" pointer-events:none; display:table;
                         position:fixed; top:0px; left:0px; border:0; width:100vw; height:100vh">

							<img src="" id="controls_keyboard_panel" style="position:absolute; opacity:0.5;">

						</div> <!-- touch_keyboard_gfx -->

					</div> <!--p8_playarea -->
				</div> <!-- p8_container -->

			</div> <!-- p8_frame -->
		</div> <!-- size limit -->

		<script type="text/javascript">p8_update_layout(); p8_update_button_icons();

			var canvas = document.getElementById("canvas");
			Module = {};
			Module.canvas = canvas;

			// pointer lock request needs to be inside a canvas interaction event
			// pico8_state.request_pointer_lock is true when 0x5f2d bit 0 and bit 2 are set -- poke(0x5f2d,0x5) 
			// note on mouse acceleration for future: // https://github.com/w3c/pointerlock/pull/49
			canvas.addEventListener("click", function () {
				if (!p8_touch_detected)
					if (pico8_state.request_pointer_lock) {
						// console.log("requesting lock");
						canvas.requestPointerLock();
					}
			});

			// Function to send the "run" command programmatically
			function p8_send_run_command() {
				console.log("Sending 'run' command to PICO-8");
				// Add each character of "run" to the key buffer
				codo_key_buffer.push("r".charCodeAt(0));
				codo_key_buffer.push("u".charCodeAt(0));
				codo_key_buffer.push("n".charCodeAt(0));
				// Add Enter key (character code 13)
				codo_key_buffer.push(13);
			}

			// Function to programmatically load a cart via URL using the drop mechanism
			function p8_load_cart_from_url(cartUrl, autoRun = true) {
				console.log("Loading cart from URL:", cartUrl);
				p8_dropped_cart_name = cartUrl.split('/').pop() || "cart.p8.png";
				p8_dropped_cart = cartUrl;
				codo_command = 9; // Use the same command as drop file

				// If autoRun is enabled, schedule the run command after a brief delay
				if (autoRun) {
					setTimeout(() => {
						p8_send_run_command();
					}, 500); // Wait 1 second for cart to load before running
				}
			}

			// Check for cart URL parameter and load it
			const urlParams = new URLSearchParams(window.location.search);
			const cartUrl = urlParams.get('cart');
			const autoRun = urlParams.get('autorun') !== 'false'; // Default to true unless explicitly set to false
			const autoStart = urlParams.get('autostart') === 'true'; // Auto-start the game engine
			if (cartUrl) {
				// Store the cart URL to be loaded when the system is ready
				window.p8_pending_cart_url = cartUrl;
				window.p8_pending_auto_run = autoRun;
				console.log("Cart URL queued for loading:", cartUrl, "Auto-run:", autoRun);
			}

			// 2. 如果 cart 参数存在，立即加载并运行
			if (cartUrl && autoStart) {
   				console.log("Auto-starting PICO-8 with cart:", cartUrl);
				p8_create_audio_context(); // 初始化音频（必须）
    			p8_run_cart('./js/pico8_edu_0206c_dev8.js', '0', cartUrl); // 加载卡带


			}

			// Listen for auto-start messages from parent window (for smooth animations)
			window.addEventListener('message', function (event) {
				if (event.data && event.data.action === 'autostart') {
					console.log("Received auto-start message from parent window");
					if (!p8_is_running) {
						p8_create_audio_context();
						p8_run_cart('./js/pico8_edu_0206c_dev8.js', '0', '/bbs/cposts/bad_lid_0_.png');
					}
				}
			});

			Module.arguments = ["/bbs/cposts/bad_lid_0_.png"];</script>
	</div>

<!--	<div style="width:320px; display:table; top:0px; position:absolute">-->
<!--		<img id="zoot" onclick="bunny_hide_or_say(help_index);" src="https://www.pico-8-edu.com/gfx/zoot_idle.gif"-->
<!--			style="cursor:pointer; display:table; width:48px; height:48px;-->
<!--max-width:6vw; max-height:6vw; margin:4px">-->

<!--		<div id="speech_bubble"-->
<!--			style="display:none; min-width:200px; max-width:80vw; background-color:#fff0e8; float:left; padding:16px; color:#222; border-radius: 10px; margin-left:10px;">-->
<!--		</div>-->
<!--	</div><img src="https://www.pico-8-edu.com/gfx/zoot_talking.gif"-->
<!--		style="width:1px; height:1px; display:none; position:absolute">-->
<!--	<script>-->

<!--		// give a second to load talking animation-->
<!--		bunny_timeout_id = setTimeout(function () {-->

<!--			// annoying-->
<!--			// bunny_say(help_about, 'MORE TIPS');-->

<!--			addEventListener("touchstart", function (event) {-->
<!--				if (!showed_touch_message)-->
<!--					bunny_say('Sorry, touchscreen typing is not available ~ please plug in a keyboard!');-->
<!--				showed_touch_message = true;-->
<!--			});-->

<!--		}, 1000);-->

<!--	</script>-->
</body>

</html>